Skip to content

[codex] improve mobile workspace UX - #556

Merged
urjitc merged 4 commits into
mainfrom
codex/mobile-workspace-ux
Jul 1, 2026
Merged

[codex] improve mobile workspace UX#556
urjitc merged 4 commits into
mainfrom
codex/mobile-workspace-ux

Conversation

@urjitc

@urjitc urjitc commented Jun 30, 2026

Copy link
Copy Markdown
Member

Summary

  • Add a dedicated mobile workspace shell with a simplified top bar and chat overlay.
  • Introduce workspace view policy for mobile-only behavior like hiding capture tools and context menus.
  • Rework mobile workspace item cards into compact icon rows while preserving desktop card previews.

Why

Mobile users should browse a workspace or chat without cramped desktop chrome, and item rows should be easier to scan and tap. Desktop behavior remains intact, including thumbnails, context menus, and title rename affordances.

Changes

  • Mobile workspace layout uses a separate shell path from desktop.
  • Mobile view policy disables capture and context-menu affordances where they do not fit.
  • Empty workspace/folder states can trigger upload directly.
  • Mobile item cards use icon-only previews, larger right-side row actions, and no title-click rename.
  • Home header/search behavior is adjusted for mobile.

Testing

  • pnpm verify

Review Notes

Start with WorkspaceLayout.tsx, WorkspaceMobileLayout.tsx, workspace-view-policy.tsx, and the item card chrome files.


View with Codesmith Autofix with Codesmith
Need help on this PR? Tag /codesmith with what you need. Autofix is disabled.

Review in cubic

Summary by CodeRabbit

  • New Features

    • Added a mobile-friendly workspace layout with a simplified header, back navigation, sharing, profile access, and chat handling.
    • Empty workspace and folder states now support click-to-upload actions.
    • Workspace browsing, item cards, and viewers now adapt more intelligently to available actions such as context menus and file capture.
  • Bug Fixes

    • Improved responsive behavior across workspace toolbars, previews, and action menus on smaller screens.
    • Capture controls and related overlays now appear only when supported, reducing unavailable controls.

@coderabbitai

coderabbitai Bot commented Jun 30, 2026

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 7d3f904b-3230-4824-a201-953d29fcd4ca

📥 Commits

Reviewing files that changed from the base of the PR and between 67088ea and 9d330c0.

📒 Files selected for processing (7)
  • src/features/workspaces/components/WorkspaceContent.tsx
  • src/features/workspaces/components/WorkspaceFileViewer.tsx
  • src/features/workspaces/components/WorkspaceItemCard.tsx
  • src/features/workspaces/components/WorkspaceLayout.tsx
  • src/features/workspaces/components/WorkspaceMobileLayout.tsx
  • src/features/workspaces/components/WorkspacePdfViewer.tsx
  • src/features/workspaces/components/WorkspaceUploadClickTarget.tsx

📝 Walkthrough

Walkthrough

Introduces a workspace view-policy module exposing viewport mode ("desktop"/"mobile") and capability flags (context menus, file capture) via React context. Wires capabilities into viewers, item cards, and content browsing to gate context menus and capture UI, adds a mobile layout, an upload click target component, and applies responsive Tailwind styling adjustments.

Changes

Workspace view capabilities and mobile layout

Layer / File(s) Summary
View policy and capabilities context
src/features/workspaces/components/workspace-view-policy.tsx
New module providing viewport-mode detection via matchMedia, capability objects, WorkspaceViewCapabilitiesProvider, useWorkspaceViewPolicy, and useWorkspaceViewCapabilities.
Layout wiring and mobile layout
src/features/workspaces/components/WorkspaceLayout.tsx, WorkspaceMobileLayout.tsx
WorkspaceShell branches rendering by viewport mode, wrapping presentation content in the capabilities provider; new WorkspaceMobileLayout renders a mobile shell with header, share, profile, and chat overlay.
Context menu and capture gating in viewers/browsing
WorkspaceContent.tsx, WorkspaceFileViewer.tsx, WorkspaceImageViewer.tsx, WorkspacePdfViewer.tsx, WorkspaceFileToolbar.tsx, WorkspaceItemToolbarSlot.tsx
Components read capabilities to conditionally render ContextMenu wrappers and gate file-capture UI/toolbar controls; capture props become optional and toolbar registration handles undefined capture.
Upload click target and empty-state wiring
WorkspaceUploadClickTarget.tsx, WorkspaceContent.tsx, WorkspaceRootEmptyPreview.tsx
New button-based WorkspaceUploadClickTarget triggers uploads; browse/root empty states wrap content with it and update helper text.
Item card title, preview controls, and stage refactor
WorkspaceItemCard.tsx, workspace-item-card-preview-stage.tsx, WorkspaceItemPreviewSurface.tsx
Item card gates context menu via capabilities, extracts WorkspaceItemCardTitle and preview-controls layer, simplifies preview stage, and adds mobile/desktop preview wrapper.
Responsive styling adjustments
AppShell.tsx, WorkspaceHomePage.tsx, AiChatPanelToolbar.tsx, workspace-item-card-chrome.ts, workspace-item-card-footer.tsx, workspace-item-card-preview-controls.tsx
Tailwind classes updated for responsive sm: breakpoint behavior across header, navbar/search input, toolbar button, and card chrome.

Estimated code review effort: 3 (Moderate) | ~30 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Shell as WorkspaceShell
  participant Policy as useWorkspaceViewPolicy
  participant Mobile as WorkspaceMobileLayout
  participant Provider as WorkspaceViewCapabilitiesProvider
  participant Viewer as WorkspacePdfViewer/WorkspaceItemCard

  Shell->>Policy: read viewportMode and capabilities
  Policy-->>Shell: viewportMode, capabilities
  alt viewportMode is mobile
    Shell->>Mobile: render mobile shell with contextBar, standardTabPanes, chatPanel
  else viewportMode is desktop
    Shell->>Shell: render docked/maximized presentation
  end
  Shell->>Provider: wrap presentationContent with capabilities
  Provider->>Viewer: expose capabilities via context
  Viewer->>Viewer: conditionally render ContextMenu and capture UI
Loading

Suggested labels: capy

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/mobile-workspace-ux

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@urjitc
urjitc marked this pull request as ready for review June 30, 2026 23:12
@urjitc

urjitc commented Jun 30, 2026

Copy link
Copy Markdown
Member Author

@greptile

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 67088ea060

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

onOpenChat,
}: WorkspaceMobileLayoutProps) {
const [shareOpen, setShareOpen] = useState(false);
const isChatOpen = chatSurfaceMode !== "hidden";

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep the mobile chat closed for the docked default

On mobile, this treats every non-hidden mode as an open full-screen overlay, but the normalized default session uses chatSurfaceMode: "docked". As a result, any first visit on a phone (or any user whose desktop state is docked) immediately renders the fixed z-50 chat over the workspace and hides the Chat button, so the new mobile browse UI is covered until the user discovers and closes chat; normalize docked to closed on mobile or only open the overlay for fullscreen.

Useful? React with 👍 / 👎.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/features/workspaces/components/workspace-view-policy.tsx`:
- Around line 38-39: The server snapshot in getWorkspaceViewportServerSnapshot()
is hardcoded to "desktop", which causes mobile hydration flicker; update this
function to derive the initial WorkspaceViewportMode from a server-side viewport
hint or use a neutral fallback until the client can determine the real mode.
Keep the fix localized to getWorkspaceViewportServerSnapshot() and the workspace
viewport policy logic so the server-rendered shell matches the eventual client
viewport as closely as possible.

In `@src/features/workspaces/components/WorkspaceContent.tsx`:
- Around line 354-358: The uploadable empty-state wrapper in WorkspaceContent
breaks the flex sizing applied by Empty because WorkspaceUploadClickTarget
becomes the flex child instead of the Empty component itself. Update the
canUpload branch in WorkspaceContent so the wrapped empty state still fills the
browse column, either by moving the flex-growing class/style onto
WorkspaceUploadClickTarget or by wrapping the empty state in a flex container
that preserves the existing flex-1 behavior.

In `@src/features/workspaces/components/WorkspaceFileViewer.tsx`:
- Around line 59-67: The two branches in WorkspaceFileViewer should keep the
same outer viewer shell so layout and overflow behavior stay consistent. Update
the no-context-menu path to use the same h-full min-h-0 overflow-hidden wrapper
as the ContextMenuTrigger path, while preserving the existing viewerContent
placement inside WorkspaceFileViewer. Ensure both the context-menu and
non-context-menu branches share the same shell structure, differing only by the
presence of ContextMenu/ContextMenuTrigger.

In `@src/features/workspaces/components/WorkspaceHomePage.tsx`:
- Around line 90-91: The workspace-list search is currently hidden on mobile
because the search container in WorkspaceHomePage is set to hide below the sm
breakpoint, and WorkspaceMobileLayout does not provide an equivalent control.
Update WorkspaceHomePage so the workspace filter/search remains available on
mobile, or add a mobile replacement in WorkspaceMobileLayout, and make sure the
workspace-list search stays separate from the WorkspaceContextBar search used
inside an open workspace.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 452cacc4-054a-48e7-85cb-589ddd337ddf

📥 Commits

Reviewing files that changed from the base of the PR and between f71343d and 67088ea.

📒 Files selected for processing (20)
  • src/components/AppShell.tsx
  • src/features/workspaces/components/WorkspaceContent.tsx
  • src/features/workspaces/components/WorkspaceFileToolbar.tsx
  • src/features/workspaces/components/WorkspaceFileViewer.tsx
  • src/features/workspaces/components/WorkspaceHomePage.tsx
  • src/features/workspaces/components/WorkspaceImageViewer.tsx
  • src/features/workspaces/components/WorkspaceItemCard.tsx
  • src/features/workspaces/components/WorkspaceItemPreviewSurface.tsx
  • src/features/workspaces/components/WorkspaceItemToolbarSlot.tsx
  • src/features/workspaces/components/WorkspaceLayout.tsx
  • src/features/workspaces/components/WorkspaceMobileLayout.tsx
  • src/features/workspaces/components/WorkspacePdfViewer.tsx
  • src/features/workspaces/components/WorkspaceRootEmptyPreview.tsx
  • src/features/workspaces/components/WorkspaceUploadClickTarget.tsx
  • src/features/workspaces/components/ai-chat/AiChatPanelToolbar.tsx
  • src/features/workspaces/components/workspace-item-card-chrome.ts
  • src/features/workspaces/components/workspace-item-card-footer.tsx
  • src/features/workspaces/components/workspace-item-card-preview-controls.tsx
  • src/features/workspaces/components/workspace-item-card-preview-stage.tsx
  • src/features/workspaces/components/workspace-view-policy.tsx

Comment on lines +38 to +39
function getWorkspaceViewportServerSnapshot(): WorkspaceViewportMode {
return "desktop";

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
fd -i 'entry*' .
rg -n --hidden -S 'renderToPipeableStream|renderToReadableStream|hydrateRoot|createRoot' .
rg -n --hidden -S 'WorkspaceShell|WorkspaceLayout' .

Repository: ThinkEx-OSS/thinkex

Length of output: 3291


Avoid hardcoding the server viewport snapshot to desktop. getWorkspaceViewportServerSnapshot() always returns "desktop", so hydrated mobile sessions will briefly render the desktop shell before switching to mobile. Seed the server snapshot from a viewport hint or return a neutral shell until the client value is known.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/features/workspaces/components/workspace-view-policy.tsx` around lines 38
- 39, The server snapshot in getWorkspaceViewportServerSnapshot() is hardcoded
to "desktop", which causes mobile hydration flicker; update this function to
derive the initial WorkspaceViewportMode from a server-side viewport hint or use
a neutral fallback until the client can determine the real mode. Keep the fix
localized to getWorkspaceViewportServerSnapshot() and the workspace viewport
policy logic so the server-rendered shell matches the eventual client viewport
as closely as possible.

Comment thread src/features/workspaces/components/WorkspaceContent.tsx
Comment thread src/features/workspaces/components/WorkspaceFileViewer.tsx
Comment on lines +90 to +91
<div className="hidden w-full min-w-0 items-center justify-center sm:flex">
<div className="relative w-full min-w-0 max-w-72">

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚀 Performance & Scalability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check whether the mobile layout exposes an alternate search entry point.
rg -n "search" -i src/features/workspaces/components/WorkspaceMobileLayout.tsx 2>/dev/null
fd -p WorkspaceMobileLayout.tsx

Repository: ThinkEx-OSS/thinkex

Length of output: 218


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- outline WorkspaceMobileLayout.tsx ---'
ast-grep outline src/features/workspaces/components/WorkspaceMobileLayout.tsx --view expanded || true

echo '--- related workspace components with search/filter terms ---'
rg -n -i "search|filter" src/features/workspaces/components src/features/workspaces 2>/dev/null || true

echo '--- file size ---'
wc -l src/features/workspaces/components/WorkspaceMobileLayout.tsx || true

Repository: ThinkEx-OSS/thinkex

Length of output: 50375


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '1,220p' src/features/workspaces/components/WorkspaceMobileLayout.tsx
printf '\n---\n'
rg -n -i "search|filter" src/features/workspaces -g '!**/node_modules/**' || true

Repository: ThinkEx-OSS/thinkex

Length of output: 38658


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- WorkspaceContextBar excerpt ---'
sed -n '60,210p' src/features/workspaces/components/WorkspaceContextBar.tsx

echo '\n--- WorkspaceContextActions excerpt ---'
sed -n '1,120p' src/features/workspaces/components/WorkspaceContextActions.tsx

Repository: ThinkEx-OSS/thinkex

Length of output: 6411


Keep workspace-list search available on mobile. src/features/workspaces/components/WorkspaceHomePage.tsx:90-91 hides the only workspace filter below sm, and src/features/workspaces/components/WorkspaceMobileLayout.tsx doesn’t add a replacement. The search action in WorkspaceContextBar is for items inside an open workspace, not the workspace list.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/features/workspaces/components/WorkspaceHomePage.tsx` around lines 90 -
91, The workspace-list search is currently hidden on mobile because the search
container in WorkspaceHomePage is set to hide below the sm breakpoint, and
WorkspaceMobileLayout does not provide an equivalent control. Update
WorkspaceHomePage so the workspace filter/search remains available on mobile, or
add a mobile replacement in WorkspaceMobileLayout, and make sure the
workspace-list search stays separate from the WorkspaceContextBar search used
inside an open workspace.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

7 issues found across 20 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="src/features/workspaces/components/WorkspaceHomePage.tsx">

<violation number="1" location="src/features/workspaces/components/WorkspaceHomePage.tsx:90">
P2: The workspace list search/filter is completely hidden below the `sm` breakpoint (`hidden ... sm:flex`), and no replacement is provided in the mobile layout. Mobile users lose the ability to search/filter their workspace list entirely.</violation>
</file>

<file name="src/features/workspaces/components/WorkspaceImageViewer.tsx">

<violation number="1" location="src/features/workspaces/components/WorkspaceImageViewer.tsx:57">
P3: Clear capture state when file capture becomes disabled; otherwise resizing back to desktop can unexpectedly restore an old hidden capture session.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread src/features/workspaces/components/WorkspacePdfViewer.tsx Outdated
Comment thread src/features/workspaces/components/WorkspaceLayout.tsx Outdated
Comment thread src/features/workspaces/components/WorkspaceUploadClickTarget.tsx Outdated
Comment thread src/features/workspaces/components/WorkspaceFileViewer.tsx Outdated
return (
<div className="flex w-full min-w-0 items-center justify-center">
<div className="relative w-full min-w-0 max-w-56 sm:max-w-72">
<div className="hidden w-full min-w-0 items-center justify-center sm:flex">

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The workspace list search/filter is completely hidden below the sm breakpoint (hidden ... sm:flex), and no replacement is provided in the mobile layout. Mobile users lose the ability to search/filter their workspace list entirely.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/features/workspaces/components/WorkspaceHomePage.tsx, line 90:

<comment>The workspace list search/filter is completely hidden below the `sm` breakpoint (`hidden ... sm:flex`), and no replacement is provided in the mobile layout. Mobile users lose the ability to search/filter their workspace list entirely.</comment>

<file context>
@@ -87,8 +87,8 @@ function WorkspaceHomeNavbarControls({
 	return (
-		<div className="flex w-full min-w-0 items-center justify-center">
-			<div className="relative w-full min-w-0 max-w-56 sm:max-w-72">
+		<div className="hidden w-full min-w-0 items-center justify-center sm:flex">
+			<div className="relative w-full min-w-0 max-w-72">
 				<Search
</file context>

Comment thread src/features/workspaces/components/WorkspaceContent.tsx Outdated
const [status, setStatus] = useState<"loading" | "ready" | "error">("loading");
const [isCaptureActive, setIsCaptureActive] = useState(false);
const enableFileCapture = useWorkspaceViewCapabilities().fileCapture;
const captureActive = enableFileCapture && isCaptureActive;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: Clear capture state when file capture becomes disabled; otherwise resizing back to desktop can unexpectedly restore an old hidden capture session.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/features/workspaces/components/WorkspaceImageViewer.tsx, line 57:

<comment>Clear capture state when file capture becomes disabled; otherwise resizing back to desktop can unexpectedly restore an old hidden capture session.</comment>

<file context>
@@ -52,16 +53,26 @@ function WorkspaceImageViewerContent({
 	const [status, setStatus] = useState<"loading" | "ready" | "error">("loading");
 	const [isCaptureActive, setIsCaptureActive] = useState(false);
+	const enableFileCapture = useWorkspaceViewCapabilities().fileCapture;
+	const captureActive = enableFileCapture && isCaptureActive;
+	const exitCapture = useCallback(() => {
+		setIsCaptureActive(false);
</file context>

@greptile-apps

greptile-apps Bot commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds a dedicated mobile workspace experience: a new WorkspaceMobileLayout shell, a workspace-view-policy hook/context that gates context menus and file-capture behind a (min-width: 640px) media query, compact row-style item cards on mobile, and a click-to-upload affordance on empty workspace states.

  • Layout split: WorkspaceShell now branches on viewportMode === \"mobile\" (from useSyncExternalStore) before falling into the existing maximized/split/standard desktop paths; WorkspaceViewCapabilitiesProvider wraps all paths so child components can conditionally opt out of context menus and capture UI.
  • Card rework: Item cards switch from flex-col (desktop) to flex-row (mobile) via Tailwind sm: tokens; preview controls are extracted from the stage into a sibling absolutely-positioned layer; WorkspaceItemCardTitle uses a CSS-only sm:hidden/hidden sm:block split to show a plain span on mobile and the rename button on desktop.
  • Upload affordance: New WorkspaceUploadClickTarget wraps empty workspace/folder states so clicking anywhere triggers the file picker; WorkspaceRootEmptyPreview and WorkspaceBrowseEmptyState now receive an onUploadFiles callback.

Confidence Score: 4/5

Safe to merge; all three findings are minor style/polish items with no data-loss or broken-flow risk.

The core branching logic (useSyncExternalStore, context provider placement, capability checks in each viewer) is solid and well-separated. The three comments are small: a div[role=button] that could be a real button, a missing overflow-hidden on the mobile file-viewer wrapper, and a misplaced dialog inside a header. None affect correctness of the mobile workspace flow.

WorkspaceUploadClickTarget.tsx (accessibility element choice) and WorkspaceFileViewer.tsx (missing overflow containment on mobile path).

Important Files Changed

Filename Overview
src/features/workspaces/components/workspace-view-policy.tsx New file: defines useWorkspaceViewPolicy via useSyncExternalStore on a (min-width: 640px) media query and a context/hook pair for gating contextMenus and fileCapture features.
src/features/workspaces/components/WorkspaceMobileLayout.tsx New file: dedicated mobile shell with sticky header and fixed full-screen chat overlay; WorkspaceShareDialog is mounted inside the <header> element rather than alongside it.
src/features/workspaces/components/WorkspaceLayout.tsx Adds mobile layout branch guarded by viewportMode === "mobile" before the existing maximized/split/standard paths; correctly places WorkspaceViewCapabilitiesProvider around all presentation content.
src/features/workspaces/components/WorkspaceUploadClickTarget.tsx New file: accessible click-to-upload wrapper using div[role="button"] with keyboard handling; could use a native <button> element instead.
src/features/workspaces/components/WorkspaceFileViewer.tsx Mobile path skips ContextMenu wrapper; the desktop path wrapped viewer content in a section.overflow-hidden but the new mobile wrapper div omits overflow-hidden.
src/features/workspaces/components/WorkspaceItemCard.tsx Extracts preview controls out of the stage into a sibling layer; introduces WorkspaceItemCardTitle that renders a span (mobile) or rename button (desktop) via CSS; context menu wrapper skipped on mobile.
src/features/workspaces/components/workspace-item-card-chrome.ts Updated token classes to support the new mobile row layout: card switches to flex-row on mobile, controls are always visible at right-2, preview stage is fixed 56px wide, grid is single-column.

Fix All in Cursor

Reviews (1): Last reviewed commit: "fix(workspaces): improve mobile item row..." | Re-trigger Greptile

@greptile-apps

greptile-apps Bot commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

T-Rex pricing update — T-Rex was free through June 2026. Effective July 1, 2026, T-Rex adds 2 credits on top of the standard 1-credit review (3 total). T-Rex settings

Comment on lines +27 to +41
return (
<div
role="button"
tabIndex={0}
className={cn(
"cursor-pointer rounded-lg outline-none focus-visible:ring-3 focus-visible:ring-ring/50",
className,
)}
aria-label={ariaLabel}
onClick={onUploadFiles}
onKeyDown={handleKeyDown}
>
{children}
</div>
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Using div[role="button"] instead of a native <button> means the element misses built-in focus management, implicit form association, and the browser's native activation semantics (e.g. Space fires click on keyup for real buttons, not keydown). Replacing with <button type="button"> achieves the same result with better semantics and avoids the manual onKeyDown handler entirely.

Suggested change
return (
<div
role="button"
tabIndex={0}
className={cn(
"cursor-pointer rounded-lg outline-none focus-visible:ring-3 focus-visible:ring-ring/50",
className,
)}
aria-label={ariaLabel}
onClick={onUploadFiles}
onKeyDown={handleKeyDown}
>
{children}
</div>
);
return (
<button
type="button"
className={cn(
"cursor-pointer rounded-lg outline-none focus-visible:ring-3 focus-visible:ring-ring/50",
className,
)}
aria-label={ariaLabel}
onClick={onUploadFiles}
>
{children}
</button>
);

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Fix in Cursor

Comment on lines +59 to +61
if (!viewCapabilities.contextMenus) {
return <div className="h-full min-h-0">{viewerContent}</div>;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 The desktop path wraps viewerContent inside a ContextMenuTrigger rendered as section.overflow-hidden, which clamps any viewer overflow. The new mobile path wraps the same content in a plain div without overflow-hidden, so a misbehaving or oversized viewer (e.g. a PDF page wider than the viewport) could bleed out of its container on mobile.

Suggested change
if (!viewCapabilities.contextMenus) {
return <div className="h-full min-h-0">{viewerContent}</div>;
}
if (!viewCapabilities.contextMenus) {
return <div className="h-full min-h-0 overflow-hidden">{viewerContent}</div>;
}

Fix in Cursor

Comment on lines +74 to +81
<WorkspaceShareDialog
membershipRole={workspace.membershipRole}
onOpenChange={setShareOpen}
open={shareOpen}
workspaceId={workspace.id}
workspaceName={workspace.name}
/>
</header>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 WorkspaceShareDialog is mounted inside the <header> element. Radix portals the dialog to document.body so it renders correctly, but semantically a dialog is not header content. Moving it to be a sibling of WorkspaceFrame (or just outside the <header>) would make the intent clearer and avoids lint rules or future tooling that validates landmark content.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Fix in Cursor

@greptile-apps

greptile-apps Bot commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds a dedicated mobile workspace experience by introducing a WorkspaceMobileLayout shell, a workspace-view-policy context that detects viewport size via useSyncExternalStore, and a suite of responsive tweaks to item cards, file viewers, and the empty-state upload affordance.

  • WorkspaceMobileLayout provides a sticky header with share/profile/chat controls; the chat panel opens as a fullscreen overlay and is dismissible via the existing close button in AiChatPanelToolbar.
  • Item cards switch from a vertical card layout to a horizontal icon-row on mobile, with preview controls always visible and rename-on-title-click disabled; capture tools (PDF/image region select) are gated behind viewCapabilities.fileCapture.
  • Empty workspace and folder states now act as click targets that trigger file upload when the viewer has mutation access.

Confidence Score: 4/5

The change is additive and the mobile path is well-isolated; desktop behavior is unchanged.

The overall change is well-structured: mobile and desktop paths are cleanly separated, capability gates are consistently applied across all file viewers, and the new upload click target has correct accessibility attributes. Two issues are worth addressing before shipping: the hydration path in workspace-view-policy.tsx will produce a visible shell swap on mobile, and the content-center class on CardHeader in WorkspaceItemCard.tsx is a no-op on single-line flex columns.

workspace-view-policy.tsx and WorkspaceItemCard.tsx warrant a quick look before merging.

Important Files Changed

Filename Overview
src/features/workspaces/components/workspace-view-policy.tsx New file introducing viewport detection via useSyncExternalStore; server snapshot always returns "desktop" which causes a layout flash on mobile during hydration
src/features/workspaces/components/WorkspaceMobileLayout.tsx New mobile-only shell with sticky header, share dialog, and fullscreen chat overlay; WorkspaceShareDialog declared inside header element (minor semantic oddity, functionally fine via portal)
src/features/workspaces/components/WorkspaceLayout.tsx Adds mobile branch to WorkspaceShell presentation switch; contextBar/standardTabPanes extracted to variables to avoid duplication; WorkspaceViewCapabilitiesProvider correctly wraps all presentation content
src/features/workspaces/components/WorkspaceItemCard.tsx Refactors card title into WorkspaceItemCardTitle (mobile span + desktop rename button) and separates preview controls from preview stage; CardHeader uses content-center which has no effect on single-line flex-col
src/features/workspaces/components/workspace-item-card-chrome.ts Responsive token updates: card switches from flex-col (desktop) to flex-row (mobile), controls are always-visible on mobile and hover-only on desktop, icon sizes adjusted
src/features/workspaces/components/WorkspaceContent.tsx Context menus conditionally rendered based on viewCapabilities; empty state now supports click-to-upload; canMutateContent guard is correctly propagated to WorkspaceBrowseEmptyState
src/features/workspaces/components/WorkspaceUploadClickTarget.tsx New accessible click target for upload zones; correctly uses role="button", tabIndex, Enter/Space keyboard handler, and aria-label
src/features/workspaces/components/WorkspaceImageViewer.tsx fileCapture capability gates all capture UI (toolbar button, overlay, shortcuts, frame); stable callbacks extracted with useCallback
src/features/workspaces/components/WorkspacePdfViewer.tsx enableFileCapture flag threaded through document loader and page components; capture shortcuts and overlay gated correctly
src/features/workspaces/components/WorkspaceItemPreviewSurface.tsx WorkspaceItemMobileIconDesktopPreview wrapper renders icon-only on mobile and existing preview content on desktop
src/features/workspaces/components/WorkspaceFileViewer.tsx Context menu wrapper now conditionally applied based on viewCapabilities.contextMenus; viewerContent extracted to avoid duplication
src/features/workspaces/components/WorkspaceHomePage.tsx Search bar hidden on mobile and max-width cap removed; input height normalized to h-8 across breakpoints
src/features/workspaces/components/WorkspaceRootEmptyPreview.tsx Empty preview overlay is now a clickable upload target; removes pointer-events-none from overlay, wires onUploadFiles prop
src/features/workspaces/components/WorkspaceFileToolbar.tsx capture prop made optional; Capture button conditionally rendered when capture is defined
src/features/workspaces/components/WorkspaceItemToolbarSlot.tsx capture prop made optional in toolbar registration; comparison logic updated to handle undefined capture
src/features/workspaces/components/workspace-item-card-preview-stage.tsx WorkspaceItemCardPreviewControls moved up to WorkspaceItemCard; preview stage now only renders the preview surface
src/components/AppShell.tsx Header styling updated: border-b removed and bg changed to bg-muted
src/features/workspaces/components/workspace-item-card-footer.tsx Type icon hidden on mobile and gap removed to save space in compact row layout
src/features/workspaces/components/workspace-item-card-preview-controls.tsx Spacer div hidden on mobile; action menu icon enlarged for easier tap targeting
src/features/workspaces/components/ai-chat/AiChatPanelToolbar.tsx Maximize/restore button hidden on mobile; close button remains visible on all breakpoints

Fix All in Cursor

Reviews (2): Last reviewed commit: "fix(workspaces): improve mobile item row..." | Re-trigger Greptile

Comment on lines +38 to +40
function getWorkspaceViewportServerSnapshot(): WorkspaceViewportMode {
return "desktop";
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 SSR hydration layout flash on mobile

getWorkspaceViewportServerSnapshot always returns "desktop", so any server-rendered (or static) page will initially mount the full desktop WorkspaceShell + WorkspaceSplitPresentation on a mobile device, then unmount it and mount WorkspaceMobileLayout after hydration fires. The swap is large enough (two different shell components) that users will see a visible layout flash and likely a reflow of the entire workspace area. Because useSyncExternalStore suppresses the mismatch warning, the issue is silent in development.

Fix in Cursor

</div>
) : null}
<CardHeader className="pointer-events-none relative z-10 shrink-0 gap-1 px-3 py-2">
<CardHeader className="pointer-events-none relative z-10 min-w-0 flex-1 content-center justify-start gap-1 py-2 pr-24 pl-3 sm:flex-none sm:shrink-0 sm:px-3">

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 content-center does not vertically center items in a flex-col container

content-center maps to align-content: center, which only affects multi-line flex / grid containers. A single-column flex container (which CardHeader is) is never multi-line, so this rule is a no-op. On mobile, where the card uses flex-row and CardHeader takes flex-1, the title and metadata will be top-aligned instead of vertically centered. Use justify-center to center content along the main (column) axis.

Suggested change
<CardHeader className="pointer-events-none relative z-10 min-w-0 flex-1 content-center justify-start gap-1 py-2 pr-24 pl-3 sm:flex-none sm:shrink-0 sm:px-3">
<CardHeader className="pointer-events-none relative z-10 min-w-0 flex-1 justify-center justify-start gap-1 py-2 pr-24 pl-3 sm:flex-none sm:shrink-0 sm:px-3">

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Fix in Cursor

Comment on lines +74 to +81
<WorkspaceShareDialog
membershipRole={workspace.membershipRole}
onOpenChange={setShareOpen}
open={shareOpen}
workspaceId={workspace.id}
workspaceName={workspace.name}
/>
</header>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 WorkspaceShareDialog rendered inside <header>

Dialog portals render at document.body regardless of where they are declared, so this works visually. However, declaring a dialog inside a <header> is semantically unexpected and makes the ownership of the state (shareOpen) harder to locate during future refactors. Moving the dialog declaration to the root of WorkspaceMobileLayout (adjacent to the chat overlay) would make the structure easier to follow.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Fix in Cursor

@github-project-automation github-project-automation Bot moved this to Backlog in Dev Board Jun 30, 2026
@urjitc
urjitc merged commit ce64ed3 into main Jul 1, 2026
6 of 7 checks passed
@urjitc
urjitc deleted the codex/mobile-workspace-ux branch July 1, 2026 03:39
@github-project-automation github-project-automation Bot moved this from Backlog to Done in Dev Board Jul 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

1 participant